﻿using System;

namespace CodeStage.AntiCheat.ObscuredTypes
{
    /// <summary>
    /// Use it instead of regular <c>uint</c> for any cheating-sensitive variables.
    /// </summary>
    /// <strong><em>Regular type is faster and memory wiser comparing to the obscured one!</em></strong>
    [Serializable]
    public struct ObscuredUInt : IEquatable<ObscuredUInt>, IFormattable
    {
        private static uint cryptoKey = 240513;

#if UNITY_EDITOR

        // For internal Editor usage only (may be useful for drawers).
        public static uint cryptoKeyEditor = cryptoKey;

#endif

        private uint currentCryptoKey;
        private uint hiddenValue;
        private uint fakeValue;
        private bool inited;

        private ObscuredUInt(uint value)
        {
            currentCryptoKey = cryptoKey;
            hiddenValue = value;
            fakeValue = 0;
            inited = true;
        }

        /// <summary>
        /// Allows to change default crypto key of this type instances. All new instances will use specified key.<br/>
        /// All current instances will use previous key unless you call ApplyNewCryptoKey() on them explicitly.
        /// </summary>
        public static void SetNewCryptoKey(uint newKey)
        {
            cryptoKey = newKey;
        }

        /// <summary>
        /// Use it after SetNewCryptoKey() to re-encrypt current instance using new crypto key.
        /// </summary>
        public void ApplyNewCryptoKey()
        {
            if (currentCryptoKey != cryptoKey)
            {
                hiddenValue = Encrypt(InternalDecrypt(), cryptoKey);
                currentCryptoKey = cryptoKey;
            }
        }

        /// <summary>
        /// Simple symmetric encryption, uses default crypto key.
        /// </summary>
        /// <returns>Encrypted <c>uint</c>.</returns>
        public static uint Encrypt(uint value)
        {
            return Encrypt(value, 0);
        }

        /// <summary>
        /// Simple symmetric encryption, uses default crypto key.
        /// </summary>
        /// <returns>Decrypted <c>uint</c>.</returns>
        public static uint Decrypt(uint value)
        {
            return Decrypt(value, 0);
        }

        /// <summary>
        /// Simple symmetric encryption, uses passed crypto key.
        /// </summary>
        /// <returns>Encrypted <c>uint</c>.</returns>
        public static uint Encrypt(uint value, uint key)
        {
            if (key == 0)
            {
                return value ^ cryptoKey;
            }
            return value ^ key;
        }

        /// <summary>
        /// Simple symmetric encryption, uses passed crypto key.
        /// </summary>
        /// <returns>Decrypted <c>uint</c>.</returns>
        public static uint Decrypt(uint value, uint key)
        {
            if (key == 0)
            {
                return value ^ cryptoKey;
            }
            return value ^ key;
        }

        /// <summary>
        /// Allows to pick current obscured value as is.
        /// </summary>
        /// Use it in conjunction with SetEncrypted().<br/>
        /// Useful for saving data in obscured state.
        public uint GetEncrypted()
        {
            ApplyNewCryptoKey();

            return hiddenValue;
        }

        /// <summary>
        /// Allows to explicitly set current obscured value.
        /// </summary>
        /// Use it in conjunction with GetEncrypted().<br/>
        /// Useful for loading data stored in obscured state.
        public void SetEncrypted(uint encrypted)
        {
            inited = true;
            hiddenValue = encrypted;
            if (Detectors.ObscuredCheatingDetector.isRunning)
            {
                fakeValue = InternalDecrypt();
            }
        }

        private uint InternalDecrypt()
        {
            if (!inited)
            {
                currentCryptoKey = cryptoKey;
                hiddenValue = Encrypt(0);
                fakeValue = 0;
                inited = true;
            }

            uint key = cryptoKey;

            if (currentCryptoKey != cryptoKey)
            {
                key = currentCryptoKey;
            }

            uint decrypted = Decrypt(hiddenValue, key);

            if (Detectors.ObscuredCheatingDetector.isRunning && fakeValue != 0 && decrypted != fakeValue)
            {
                Detectors.ObscuredCheatingDetector.Instance.OnCheatingDetected();
            }

            return decrypted;
        }

        #region operators, overrides, interface implementations

        //! @cond
        public static implicit operator ObscuredUInt(uint value)
        {
            ObscuredUInt obscured = new ObscuredUInt(Encrypt(value));
            if (Detectors.ObscuredCheatingDetector.isRunning)
            {
                obscured.fakeValue = value;
            }
            return obscured;
        }

        public static implicit operator uint(ObscuredUInt value)
        {
            return value.InternalDecrypt();
        }

        public static ObscuredUInt operator ++(ObscuredUInt input)
        {
            uint decrypted = input.InternalDecrypt() + 1;
            input.hiddenValue = Encrypt(decrypted, input.currentCryptoKey);

            if (Detectors.ObscuredCheatingDetector.isRunning)
            {
                input.fakeValue = decrypted;
            }
            return input;
        }

        public static ObscuredUInt operator --(ObscuredUInt input)
        {
            uint decrypted = input.InternalDecrypt() - 1;
            input.hiddenValue = Encrypt(decrypted, input.currentCryptoKey);

            if (Detectors.ObscuredCheatingDetector.isRunning)
            {
                input.fakeValue = decrypted;
            }
            return input;
        }

        /// <summary>
        /// Returns a value indicating whether this instance is equal to a specified object.
        /// </summary>
        ///
        /// <returns>
        /// true if <paramref name="obj"/> is an instance of ObscuredUInt and equals the value of this instance; otherwise, false.
        /// </returns>
        /// <param name="obj">An object to compare with this instance. </param><filterpriority>2</filterpriority>
        public override bool Equals(object obj)
        {
            if (!(obj is ObscuredUInt))
                return false;

            ObscuredUInt oi = (ObscuredUInt)obj;
            return ((int)hiddenValue == (int)oi.hiddenValue);
        }

        /// <summary>
        /// Returns a value indicating whether this instance is equal to a specified ObscuredUInt.
        /// </summary>
        ///
        /// <returns>
        /// true if <paramref name="obj"/> has the same value as this instance; otherwise, false.
        /// </returns>
        /// <param name="obj">An ObscuredUInt value to compare to this instance.</param><filterpriority>2</filterpriority>
        public bool Equals(ObscuredUInt obj)
        {
            return (int)hiddenValue == (int)obj.hiddenValue;
        }

        /// <summary>
        /// Converts the numeric value of this instance to its equivalent string representation.
        /// </summary>
        ///
        /// <returns>
        /// The string representation of the value of this instance, consisting of a sequence of digits ranging from 0 to 9, without a sign or leading zeroes.
        /// </returns>
        /// <filterpriority>1</filterpriority>
        public override string ToString()
        {
            return InternalDecrypt().ToString();
        }

        /// <summary>
        /// Converts the numeric value of this instance to its equivalent string representation using the specified format.
        /// </summary>
        ///
        /// <returns>
        /// The string representation of the value of this instance as specified by <paramref name="format"/>.
        /// </returns>
        /// <param name="format">A numeric format string.</param><exception cref="T:System.FormatException">The <paramref name="format"/> parameter is invalid. </exception><filterpriority>1</filterpriority>
        public string ToString(string format)
        {
            return InternalDecrypt().ToString(format);
        }

        /// <summary>
        /// Returns the hash code for this instance.
        /// </summary>
        ///
        /// <returns>
        /// A 32-bit signed integer hash code.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        public override int GetHashCode()
        {
            return InternalDecrypt().GetHashCode();
        }

        /// <summary>
        /// Converts the numeric value of this instance to its equivalent string representation using the specified culture-specific format information.
        /// </summary>
        ///
        /// <returns>
        /// The string representation of the value of this instance , which consists of a sequence of digits ranging from 0 to 9, without a sign or leading zeros.
        /// </returns>
        /// <param name="provider">An object that supplies culture-specific formatting information. </param><filterpriority>1</filterpriority>
        public string ToString(IFormatProvider provider)
        {
            return InternalDecrypt().ToString(provider);
        }

        /// <summary>
        /// Converts the numeric value of this instance to its equivalent string representation using the specified format and culture-specific format information.
        /// </summary>
        ///
        /// <returns>
        /// The string representation of the value of this instance as specified by <paramref name="format"/> and <paramref name="provider"/>.
        /// </returns>
        /// <param name="format">A numeric format string.</param><param name="provider">An object that supplies culture-specific formatting information about this instance. </param><exception cref="T:System.FormatException">The <paramref name="format"/> parameter is invalid. </exception><filterpriority>1</filterpriority>
        public string ToString(string format, IFormatProvider provider)
        {
            return InternalDecrypt().ToString(format, provider);
        }

        //! @endcond

        #endregion operators, overrides, interface implementations
    }
}